/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.myfaces.extensions.cdi.forge.api.support;

import org.apache.myfaces.extensions.cdi.core.api.config.view.ViewConfig;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.jboss.forge.parser.JavaParser;
import org.jboss.forge.parser.java.JavaInterface;
import org.jboss.forge.parser.java.JavaSource;
import org.jboss.forge.project.facets.JavaSourceFacet;
import org.jboss.forge.project.facets.WebResourceFacet;
import org.jboss.forge.resources.DirectoryResource;
import org.jboss.forge.resources.Resource;
import org.jboss.forge.resources.ResourceFilter;
import org.jboss.forge.resources.java.JavaResource;
import org.jboss.forge.shell.Shell;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * This is a support class that enables easy manipulation of ViewConfig classes. A given
 * ViewConfig handled by the CodiPlugin will be managed using this class.
 *
 * @author Cosmin Martinconi, 29.07.2011
 */
public class ViewConfigUtil {

    private static Shell shell;

    private JavaSource configSource;
    private ViewConfigMetadata viewConfig;

    /**
     * Constructor. Calling this constructor the class will search within the
     * current project files and prompt the user in order to choose the desired
     * ViewConfig class.
     *
     * @param shell   The current Shell instance
     */
    public ViewConfigUtil(Shell shell) {
        this(shell, ".*\\.java");
    }

    /**
     * Constructor. Calling this constructor the "configName" ViewConfig will be
     * searched in the current project.
     *
     * @param projectShell   The current Shell instance
     * @param configName     The ViewConfig name for which this util class will be created
     */
    public ViewConfigUtil(Shell projectShell, String configName) {
        shell = projectShell;
        this.configSource = getConfigSourceByName(configName);
        initViewConfig();
    }

    /**
     * Constructor. This Constructor initializes the ViewConfigUtil associating it
     * with the given ViewConfig source.
     *
     * @param projectShell   The current Shell instance
     * @param configSource   The ViewConfig source name for which this util class will be created
     */
    public ViewConfigUtil(Shell projectShell, JavaSource configSource) {
        shell = projectShell;
        this.configSource = configSource;
        initViewConfig();
    }

    /**
     * The method creates a new ViewConfigUtil instance with a new ViewConfig source.
     *
     * @param shell        The current Shell instance
     * @param configName   The name of the new ViewConfig that will be created
     * @return             The new ViewConfigUtil instance
     */
    public static ViewConfigUtil getNewViewConfig(Shell shell, String configName) {
        JavaSourceFacet javaFacet = shell.getCurrentProject().getFacet(JavaSourceFacet.class);
        JavaSource source = JavaParser.create(JavaInterface.class)
                                      .setPackage(javaFacet.getBasePackage() + ".config")
                                      .setName(configName);
        ViewConfigUtil newViewConfig = new ViewConfigUtil(shell, source);
        newViewConfig.getViewConfigMetadata().addExtensionInterface(ViewConfig.class);
        return newViewConfig;
    }

    private void initViewConfig() {
        CompilationUnit unit = (CompilationUnit)configSource.getInternal();
        TypeDeclaration typeDeclaration = (TypeDeclaration)unit.types().get(0);
        File dirFile = getWebFacet().getWebRootDirectory().getUnderlyingResourceObject();

        this.viewConfig = new ViewConfigMetadata(typeDeclaration, unit, dirFile);
    }

    /**
     * This method searches within this ViewConfigUtil for a containing sub-metadata
     * that will match the "configPath"
     *
     * @param configPath   The reference path to the child metadata of this ViewConfigUtil
     * @return             The ViewConfigMetadata instance corresponding to the "configPath"
     */
    public ViewConfigMetadata getViewConfigMetadata(String configPath) {
        List<ViewConfigMetadata> vcResList = Arrays.asList(this.getViewConfigMetadata());
        ViewConfigMetadata vcMetadata = null;
        for (String folder : configPath.split("\\.")) {
            boolean found = false;
            for (ViewConfigMetadata res : vcResList) {
                if (res.getName().equals(folder)) {
                    vcMetadata = res;
                    vcResList = vcMetadata.getMetadataChildList();
                    found = true;
                    break;
                }
            }
            if (!found) {
                throw new ViewConfigException("[" + configPath + "] could not be found");
            }
        }

        return vcMetadata;
    }

    /**
     * Saves the current state of this ViewConfigUtil
     */
    public void saveViewConfig() {
        try {
            getJavaFacet().getJavaResource(getViewConfigMetadata().toString()).setContents(this.getSource());
        } catch (FileNotFoundException e) {
            throw new ViewConfigException(e);
        }
    }

    /**
     * @return  The ViewConfigMetadata of the ViewConfig managed by this ViewConfigUtil
     */
    public ViewConfigMetadata getViewConfigMetadata() {
        return viewConfig;
    }

    /**
     * @return  The JavaSource of the ViewConfig managed by this ViewConfigUtil
     */
    public JavaSource getSource() {
        return configSource;
    }

    private  JavaSourceFacet getJavaFacet() {
        return shell.getCurrentProject().getFacet(JavaSourceFacet.class);
    }

    private  WebResourceFacet getWebFacet() {
        return shell.getCurrentProject().getFacet(WebResourceFacet.class);
    }

    /**
     * This method validates whether the "vcSource" JavaSource is a valid ViewConfig by
     * checking the super interface value that must be of "ViewConfig" type.
     *
     * @param vcSource   The JavaSource that will be validated
     * @return           true if the JavaSource is a ViewConfig,
     *                   false otherwise
     */
    public static boolean isValidViewConfig(JavaSource vcSource) {
        CompilationUnit vcUnit = (CompilationUnit)vcSource.getInternal();
        for (Object st : ((TypeDeclaration)vcUnit.types().get(0)).superInterfaceTypes()) {
            String superName = ((SimpleType)st).getName().getFullyQualifiedName();
            if (superName.equals(ViewConfig.class.getSimpleName()) || superName.equals(ViewConfig.class.getName())) {
                return true;
            }
        }
        return false;
    }

    public static List<ViewConfigUtil> getAllViewConfigs(Shell projectShell) {
        JavaSourceFacet javaFacet = projectShell.getCurrentProject().getFacet(JavaSourceFacet.class);
        List<ViewConfigUtil> viewConfigFiles = new ArrayList<ViewConfigUtil>();
        for (Resource<?> classFile : getClassResourceByName(javaFacet.getSourceFolder(), ".*\\.java")) {
            JavaSource config = null;
            try {
                config = ((JavaResource)classFile).getJavaSource();
            } catch (FileNotFoundException ignored) {}
            if (isValidViewConfig(config)) {
                viewConfigFiles.add(new ViewConfigUtil(projectShell, config));
            }
        }

        if (viewConfigFiles.isEmpty()) {
            throw new ViewConfigException("No valid ViewConfig found!");
        }
        return viewConfigFiles;
    }

    /**
     * This method iterates through all the "configName" files of the project in order to find
     * the ViewConfig instances of the project. If multiple classes found the user will be prompted
     * to select one choice.
     *
     * @param configName  The name of the file that will be searched
     * @return            The JavaSource value of the found ViewConfig.
     */
    private JavaSource getConfigSourceByName(String configName) {
        String javaFileName = configName.endsWith(".java") ? configName : configName.concat(".java");

        ArrayList<JavaSource> viewConfigFiles = new ArrayList<JavaSource>();
        List<String> fileNames = new ArrayList<String>();
        for (Resource<?> classFile : getClassResourceByName(getJavaFacet().getSourceFolder(), javaFileName)) {
            JavaSource config = null;
            try {
                config = ((JavaResource)classFile).getJavaSource();
            } catch (FileNotFoundException ignored) {}
            if (isValidViewConfig(config)) {
                viewConfigFiles.add(config);
                fileNames.add(config.getPackage() + "." + classFile.getName());
            }
        }

        if (viewConfigFiles.isEmpty()) {
            throw new ViewConfigException("No valid ViewConfig found!");
        }
        JavaSource viewConfig;
        if (viewConfigFiles.size() == 1) {
            viewConfig = viewConfigFiles.get(0);
        } else {
            System.out.println("Multiple ViewConfig files found");
            viewConfig = viewConfigFiles.get(shell.promptChoice("Which ViewConfig instance should be used? ", fileNames));
        }

        return viewConfig;
    }

    /**
     * This recursive method searches for the "name" file in the initial "searchFolder" directory.
     *
     * @param searchFolder   The folder under which the search is done. This parameter will
     *                       recursively cover all sub-folders of the initial parameter value
     * @param name           The name of the searched file
     * @return               A list of all the Resource<?> instances found for the given "name" file
     */
    private static List<Resource<?>> getClassResourceByName(Resource<?> searchFolder, final String name) {
        List<Resource<?>> classFiles;

        classFiles = searchFolder.listResources(new ResourceFilter() {
            @Override
            public boolean accept(Resource<?> resource) {
                return (resource instanceof JavaResource) && resource.getName().matches(name);
            }
        });


        for (Resource<?> resource : searchFolder.listResources(
                new ResourceFilter() {
                    @Override
                    public boolean accept(Resource<?> resource) {
                        return (resource instanceof DirectoryResource);
                    }
                })) {
            classFiles.addAll(getClassResourceByName(resource, name));
        }


        return classFiles;
    }

}
